#!/usr/bin/env python

"""
Dtella - Startup Module
Copyright (C) 2007-2008  Dtella Labs (http://www.dtella.org/)
Copyright (C) 2007-2008  Paul Marks (http://www.pmarks.net/)
Copyright (C) 2007-2008  Jacob Feisley (http://www.feisley.com/)
Copyright (C) 2009-      Dtella Cambridge (http://camdc.pcriot.com/)
Copyright (C) 2009-      Ximin Luo <infinity0x@gmail.com>
Copyright (C) 2009-      Andrew Cooper <andyhhp@hotmail.com>

$Id$

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

##
#@package Dtella
#@authors Paul Marks, Jacob Feisley, Ximin Luo, Andrew Cooper
#
#@brief Dtella entry point
#
#This is the entry point for dtella.
#
#This module is named with a capital letter to allow the test suits to import
#this file seporatly from the dtella.* package
#
#It contains a hack to fix a bug in py2app when compiled for mac.

##
#@file Dtella.py
#@authors Paul Marks, Jacob Feisley, Ximin Luo, Andrew Cooper
#
#@brief Dtella entry point
#
#This is the entry point for dtella.
#
#This module is named with a capital letter to allow the test suits to import
#this file seporatly from the dtella.* package
#
#It contains a hack to fix a bug in py2app when compiled for mac.

# When Dtella is packaged by py2app, Dtella.py and the dtella.* package are
# split into separate directories, causing the import to fail.  We'll hack
# around the problem by stripping the base directory from the path.
if __name__ == '__main__':
    try:
        import dtella.common
    except ImportError:
        import sys
        sys.path = [p for p in sys.path if p != sys.path[0]]

# Patch the twisted bugs before doing anything else.
#import dtella.common.fix_twisted

import twisted.internet.error
import twisted.python.log
from twisted.internet import reactor
import sys
import socket
import time
import getopt

from dtella.config.config import ConfigManager as ConfigManager

#from dtella.common.log import setLogFile
#from dtella.common.log import LOG


##@brief Add custom handler for twisted logging.
#
#Inserts a handler function to intercept failure messages and report them
#@param handler
def addTwistedErrorCatcher(handler):
    def logObserver(eventDict):
        if not eventDict['isError']:
            return
        try:
            text = eventDict['failure'].getTraceback()
        except KeyError:
            text = ' '.join(str(m) for m in eventDict['message'])
        handler(text)
    twisted.python.log.startLoggingWithObserver(logObserver, setStdout=False)

"""
def runBridge():
    import dtella.bridge_config as cfg
    setLogFile(cfg.file_base + ".log", 4<<20, 4)
    LOG.debug("Bridge Logging Manager Initialized")

    addTwistedErrorCatcher(LOG.critical)

    from dtella.bridge.main import DtellaMain_Bridge
    dtMain = DtellaMain_Bridge()

    from dtella.bridge.bridge_server import getServiceConfig
    scfg = getServiceConfig()
    scfg.startService(dtMain)

    reactor.run()


def runDconfigPusher():
    import dtella.bridge_config as cfg
    setLogFile(cfg.file_base + ".log", 4<<20, 4)
    LOG.debug("Dconfig Pusher Logging Manager Initialized")

    addTwistedErrorCatcher(LOG.critical)

    from dtella.bridge.push_dconfig_main import DtellaMain_DconfigPusher
    dtMain = DtellaMain_DconfigPusher()
    reactor.run()


def runClient(dc_port):
    #Logging for Dtella Client
    setLogFile("dtella.log", 1<<20, 1)
    LOG.debug("Client Logging Manager Initialized")

    from dtella.client.main import DtellaMain_Client
    dtMain = DtellaMain_Client()

    import dtella.local_config as local
    from dtella.common.util import get_version_string

    bugs_email = "bugs@dtella.org"
    try:
        if local.bugs_email:
            bugs_email = local.bugs_email
    except AttributeError:
        pass

    def botErrorReporter(text):
        dch = dtMain.dch
        if dch:
            dch.bot.say(
                "Something bad happened.  You might want to email this to "
                "%s so we'll know about it:\n"
                "Version: %s %s\n%s" %
                (bugs_email, local.hub_name, get_version_string()[3:], text))

    addTwistedErrorCatcher(botErrorReporter)
    addTwistedErrorCatcher(LOG.critical)

    if local.adc_mode:
        from dtella.client.adc import ADCFactory
        dfactory = ADCFactory(dtMain, dc_port)
    else:
        from dtella.client.dc import DCFactory
        dfactory = DCFactory(dtMain, dc_port)

    LOG.info("%s %s" % (local.hub_name, local.version))

    global exit_code
    exit_code = 0

    def cb(first):
        global exit_code
        try:
            reactor.listenTCP(dc_port, dfactory, interface='127.0.0.1')
        except twisted.internet.error.CannotListenError:
            if first:
                LOG.warning("TCP bind failed.  Killing old process...")
                if terminate(dc_port, dtMain.state.killkey):
                    LOG.info("Ok.  Sleeping...")
                    reactor.callLater(2.0, cb, False)
                else:
                    LOG.error("Kill failed.  Giving up.")
                    reactor.stop()
                    exit_code = 1
            else:
                LOG.error("Bind failed again.  Giving up.")
                reactor.stop()
                exit_code = 1
        else:
            # Kill any old Dtella processes that may be running on a different port
            if dc_port != dtMain.state.clientport:
                terminate(dtMain.state.clientport, dtMain.state.killkey)

            # set a lock
            import hashlib, random
            dtMain.state.killkey = hashlib.sha256(str(random.random())[2:] + \
                str(random.random())[2:] + str(random.random())[2:] + \
                str(random.random())[2:] + str(random.random())[2:] + \
                str(random.random())[2:]).digest()
            dtMain.state.clientport = dc_port
            dtMain.state.saveState()

            LOG.info("Listening on 127.0.0.1:%d" % dc_port)
            dtMain.startConnecting()

    reactor.callWhenRunning(cb, True)
    reactor.run()
    return exit_code


def terminate(dc_port, killkey):
    # Terminate another Dtella process on the local machine
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(('127.0.0.1', dc_port))
        import dtella.local_config as local
        from base64 import b32encode
        # the extra |$KillDtella| is so we can kill old nodes during an upgrade
        if local.adc_mode:
            sock.sendall("HKILLDTELLA %s\n|$KillDtella|" % b32encode(killkey))
        else:
            sock.sendall("$KillDtella %s|$KillDtella|" % b32encode(killkey))
        print "Sent Packet of Death on port %d..." % dc_port
        sock.shutdown(socket.SHUT_RDWR)
        sock.close()
    except socket.error:
        return False

    return True
"""
##@brief Help string
#
#String containing the command-line parameters and descriptions
help_msg = """
Dtella Help

Options:
-h, --help      Displays this message.
--terminate     Terminates all other instances of dtella.
--adc           Configure the node to interact as an ADC hub (does by default).
--nmdc          Configure the node to interact as a compatability mode hub.
--tcp-port      Sets the TCP port to use.
--udp-port      Sets the UDP port to use.
--state         Instructs the node to use a specific file for storing its state.
--log           Sets the log file to a user specified one.
--bridge        Start the node as a bridge node.  No clients may connect to a
                bridge node.
--dconfigpusher Update the DNS configuration data.
--makekey       Creates a private key for the bridge to sign packages with.
"""

class NotSupported(Exception):
    pass

##@brief Physical entry point.
#
#Parses the command-line arguments and initiates the seporate modules of dtella
#@param argv List of command-line arguments.  Must include module name as
# element 0.
#@param testonly Boolean flag to prevent execution of further functionality.
# Should only be used by the test suits for validation testing of other
# command-line argument.
#
#@return 0 No Error
#@return 1 Unknown Error
#@return 2 Invalid command-line options.
#@return 3 
#
#@sa help_msg
def main(argv=[__name__], testonly = False):
    # Parse command-line arguments
    # Options present at all points
    allowed_opts = ['--help', 'terminate']
    usage_str = "Usage: %s [--help] [--terminate]" % argv[0]
    
    # Client options
    try:
        import dtella.client
    except ImportError:
        pass
    else:
        usage_str += " [--adc] [--nmdc]"
        allowed_opts.extend(['adc', 'nmdc'])
    
    # Config options
    try:
        import dtella.config
    except ImportError:
        pass
    else:
        usage_str += " [--tcp-port=<num>] [--udp-port=<num>] " \
            "[--state=<file>] [--log=<file>]"
        allowed_opts.extend(['tcp-port=', 'udp-port=', 'state=', 'log='])

    # Bridge options
    try:
        import dtella.bridge
    except ImportError:
        pass
    else:
        usage_str += " [--bridge] [--dconfigpusher] [--makekey]"
        allowed_opts.extend(['bridge', 'dconfigpusher', 'makekey'])

    try:
        opts, args = getopt.getopt(argv[1:], 'h', allowed_opts)
    except getopt.GetoptError, err:
        print err
        print usage_str
        return 2

    opts = dict(opts)
    
    cfg = ConfigManager()
    
    #print "cfg.Get_tcp_port", cfg.Get_tcp_port()
    #print "cfg.Set_tcp_port(2)", cfg.Set_tcp_port(2)
    #print "cfg.Get_tcp_port", cfg.Get_tcp_port()
    #print "cfg.Set_tcp_port('str')", cfg.Set_tcp_port("str")
    #print "cfg.Get_tcp_port", cfg.Get_tcp_port()
    
    # Help
    if '-h' in opts or '--help' in opts:
        print help_msg
        return 0
    
    # tcp-port
    tcp_port = cfg.Get_tcp_port()
    if '--tcp-port' in opts:
        try:
            tcp_port = int(opts['--tcp-port'])
            if not (1 <= tcp_port < 65536):
                raise ValueError
        except ValueError:
            print "TCP port must be a number in the range 1-65535"
            return 3
    cfg.Set_tcp_port(tcp_port)
    
    # tcp-port
    udp_port = cfg.Get_tcp_port();
    if '--udp-port' in opts:
        try:
            udp_port = int(opts['--udp-port'])
            if not (1 <= udp_port < 65536):
                raise ValueError
        except ValueError:
            print "UDP port must be a number in the range 1-65535"
            return 3
    cfg.Set_udp_port(udp_port)

    # NMDC mode
    if '--nmdc' in opts:
        raise NotSupported
    
    # ADC mode
    if '--adc' in opts:
        pass#TODO replace with configmanager
    
    
    
    # If the module is called with the testonly flag set, ignore any remaining
    # execution as it invokes the higher level functionality
    if testonly:
        return 0
    
    
    if '--bridge' in opts:
        return runBridge()

    if '--dconfigpusher' in opts:
        return runDconfigPusher()

    if '--makeprivatekey' in opts:
        from dtella.bridge.private_key import makePrivateKey
        return makePrivateKey()


    """
    import dtella.common.state as state
    from dtella.client.main import STATE_FILE
    sm = state.StateManager(None, STATE_FILE, [state.ClientPort(), state.KillKey()])
    sm.initLoad(False)

    if not dc_port:
        dc_port = sm.clientport

    # Try to terminate an existing process
    if '--terminate' in opts:
        if terminate(dc_port, sm.killkey):
            # Give the other process time to exit first
            print "Sleeping..."
            time.sleep(2.0)
        else:
            print "Nothing to do."

        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.bind(('127.0.0.1', dc_port))
            print "TCP port %s is free." % dc_port
            sock.close()
            return 0
        except:
            print "TCP port %s is still in use." % dc_port
            return 1

    return runClient(dc_port)
    """
    return 0 #TODO remove

if __name__=='__main__':
    sys.exit(main(sys.argv))
